Skip to content

feat(composer): lock the composer and show the prompt as Sending in the feed - #1183

Merged
Juliusolsson05 merged 8 commits into
mainfrom
feat/pending-prompt-sending
Sep 25, 2026
Merged

Juliusolsson05 merged 8 commits into
mainfrom
feat/pending-prompt-sending

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Problem

Pressing Enter left the prompt in an editable composer until the provider accepted it. For Claude, acceptance is the JSONL acknowledgement, which can take seconds, and Claude painted no user row until the transcript caught up. The submit looked ignored, and the editable draft invited a second Enter or edits to a prompt already on its way.

Behavior

  • Enter locks the composer. While promptDelivery.kind === 'sending' the composer shows empty and is read-only. The key handler ignores every key, including /, Tab, arrows, Enter, Escape and Ctrl+C. Paste, type-to-focus and paste-to-focus are off.
  • The prompt moves into the feed. It shows at once as a user row at 50% opacity with a Sending… caption.
  • "Sent" = provider accepted it: Claude's JSONL acknowledgement, OpenCode/Grok/Pi's handoff, Codex's completed PTY writes.
  • All providers. Echo providers dim their existing optimistic row, which is now keyed by submission id. Claude gets a pending-only row that is added at Enter and removed when the send settles, so it can never double-render against Claude's committed row or queue-operation strip.
  • Queued behind a running turn: the composer unlocks on the queue acceptance. For Claude, the pending row goes and Claude's queue strip shows the prompt.
  • Failure: the pending row goes and the lock lifts, so the stored draft (text and images) is visible again. The existing uncertain banner still blocks a plain resend.

Design notes

  • The draft stays in the store for the whole send; only the view hides it. Autosave persists only draftInput, and a session replacement transfers only current draft fields. On acceptance, draftAfterAcceptance strips the sent prompt and keeps text that dictation, templates or reply-to-selection added at either end. submitCurrentDraft also refuses while sending, because the per-mount in-flight ref does not survive a remount.
  • Order barrier in main. session:deliver-prompt calls flushJsonl(sessionId) before replying. Otherwise Claude's acceptance reply overtakes the coalesced JSONL batch (microtask vs setImmediate), and the pending row is removed before the committed row exists.
  • Pending state is presentation, not ledger state. Feed gets pendingEntryUuid and dims the matching entry item. Ledger rows, ordering and ownership are untouched, so the D11 identity cache holds.
  • Claude's row does not reuse addOptimisticCodexUserEntry. That action's mid-turn branch writes queuedMessages, which for Claude belongs to the queue-operation reducer.
  • Ownership fixes:
    • Repeated prompts: a committed twin must be no older than the submit (2 s clock tolerance). Before, a second continue was owned by the first one and vanished at once.
    • Pasted prompts: a committed user row's key is the inner text of Claude's <pasted_content> envelope, using the same strict unwrap main uses for acceptance.
  • One shared prefix. The optimistic uuid prefix lives in session-runtime/optimisticPrompt.ts, and every copied literal is gone.

Review (one round: Claude + Codex reviewers)

Finding Resolution
Codex 1 (major): reload mid-send loses the prompt Fixed: the draft stays stored, and only the view hides it
Codex 2 (major): replacement restores a failed prompt into the retired session Fixed by the same change: nothing is restored, and the draft transfers normally
Codex 3 (minor): a pasted Claude prompt paints twice during handoff Fixed: the envelope is unwrapped for the ownership key; the test uses the recorded row
Claude 1 (minor): pending row blinks out before the committed row arrives Fixed: JSONL flush barrier before the delivery reply
Claude 2 (minor): Escape/Ctrl+C can't reach main-delivered providers while locked, and toast a false "draft preserved" Fixed: the lock swallows them too (only Enter is preventDefault-ed, so app shortcuts still work)
Claude 3 (claim gap): Grok rows have no timestamps, so the repeated-prompt fix doesn't reach Grok Not changed. There is no timestamp to compare, so Grok keeps the old presence rule. Documented below
Claude 4 (nit): one hard-coded prefix left Fixed

Tests

  • useComposerKeybinds.pendingPrompt.renderer.test.tsx. Real workspace controller, real submit hook and real ledger → Feed, with delivery held open. It covers:
    • Claude pending row and lock, with the draft kept in the store.
    • A writer adding text during the send.
    • Failure: text and images become visible again.
    • Queue acceptance.
    • Every key locked, with an unlocked control.
    • OpenCode: the optimistic row is dimmed while sending and undimmed after.
  • ownership.optimisticTwin.test.ts:
    • An older twin does not own a new submit, and the submit's own twin does.
    • Tolerance edges and missing timestamps.
    • A pasted prompt, using the recorded fixture. Confirmed it fails without the unwrap.
  • promptDeliveryDraft.test.ts: acceptance stripping.
  • streamingSubmissionIdentity: now pins distinct uuids for same-millisecond submits. It used to pin the collision.
  • npx tsc -b is clean.
  • Full npm test on Node 24 (before the review fixes): 6479 passed. The 4 failures were 5 s timeouts in untouched files (lazy-prose, settings hydration ×2, ProviderEnablementRow) and fail locally on main too. Affected areas were re-run after the fixes.

Known limitations

  • Grok keeps the old repeated-prompt behavior because its rows carry no timestamps.
  • The Sending WorkIndicator chip still shows beside the row caption. It is driven by stream phase and carries the elapsed clock.
  • An image-only Claude submit has no text row. The lock and the WorkIndicator still show that it's sending.
  • Manual app QA has not been done.

Fixes #1181

🤖 Generated with Claude Code

Juliusolsson05 and others added 8 commits September 24, 2026 18:24
Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Optimistic prompt ownership was text presence alone, so any earlier
committed user row with the same text owned a new optimistic row. The
second "continue" of a session was rejected the moment it was minted and
the prompt appeared nowhere until the transcript caught up. Once the
composer clears at Enter, that gap reads as a lost prompt.

A committed row can only be the twin of a prompt submitted at or before
it, so ownership now also requires the twin to be no older than the
submit, within a 2 s clock tolerance. Rows without timestamps keep the
old presence rule.

Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he feed

Enter used to leave the prompt in an editable composer until the
provider accepted it, which for Claude is the JSONL acknowledgement and
can take seconds. Claude also painted no user row until the transcript
caught up. The submit looked ignored and invited a second Enter.

Now Enter clears the draft (text and images) and locks the composer
read-only while promptDelivery is sending. The prompt appears in the feed
at once, dimmed with a "Sending…" caption. Echo providers dim their
existing optimistic row, now keyed by submission id. Claude gets a
pending-only row that lives exactly as long as the send, so it can never
double-render against the committed row or Claude's queue strip. A failed
send puts the text and images back into the composer. Escape and Ctrl+C
still reach the agent while locked.

The optimistic uuid prefix moves into one shared module instead of three
literals.

Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The identity test asserted that two submits in the same millisecond got
the same time-based optimistic uuid. That was a documented limit of the
diagnostics-only Stage 0. The uuid is now keyed by submission, so the
test pins the distinct rows instead.

Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clearing the draft at Enter and restoring it on failure lost prompts in
two cases found in review. A renderer reload mid-send flushed the emptied
draft through autosave, which persists only draftInput. A session
replacement mid-send transferred an empty draft and restored the failed
prompt into the retired session.

The draft now stays in the store for the whole send. Only the composer
view is emptied while locked, and acceptance strips the sent prompt while
keeping text that dictation or templates added during the send. Submit
also refuses while sending, because the per-mount in-flight ref does not
survive a remount and the hidden draft would otherwise look submittable
to the global Enter target.

The lock now also swallows Escape and Ctrl+C. Main refuses raw writes for
the whole main-owned delivery, so those keys never reached the agent and
only produced a false "draft preserved" toast.

Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude's acceptance is main seeing the prompt's JSONL line, but that line
waits in the JSONL coalescer for the next setImmediate, while the invoke
reply resumes in a microtask and overtakes it. The renderer removes its
pending Sending row when the reply lands, so the prompt blinked out of
the feed until the batch arrived. Flushing the session's buffer before
replying keeps the committed row ahead of the acceptance on the same
channel.

Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude commits a pasted prompt inside its <pasted_content> envelope while
the pending row carries the typed text, so the two never matched and both
painted until the send settled. The committed user row's ownership key is
now the envelope's inner text, using the same strict whole-string unwrap
main uses to recognise the accepted prompt. The test drives the recorded
pasted-prompt row.

Also moves the last copied optimistic prefix literal onto the shared
constant.

Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit 78cf3a8 into main Sep 25, 2026
1 of 2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the feat/pending-prompt-sending branch September 25, 2026 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(composer): lock the composer and show the prompt as Sending in the feed

1 participant